查看原文
其他

RenderThread:异步渲染动画

郭海洋 京东零售技术 2021-10-13


引言


Android中动画是比较常见的,无论你是使用的补间动画还是属性动画,都无法避免对UI的绘制,那么异步渲染是否可行呢?答案当然是可行的,其关键就是RenderThread,对于RenderThread可能有些人对它并不甚了解,下面先简要的介绍下它。

作者:郭海洋

Android 高级开发工程师,主要负责统一控件的开发和维护,以及新技术的研究和创新工作。

 RenderThread是什么


在Android5.0之前,Android的主线程同时也是Open GL线程,在Android 5.0之后,Android应用程序的Open GL线程就独立出来了,称之为Render Thread。


看到此处,你可能会有疑问,这里的Open GL线程又是什么呢?要理解此处,首先要明确硬件加速是什么,其实就是通过GPU来渲染。GPU作为一个硬件,应用程序无法直接使用,它是由GPU厂商按照Open GL规范实现的驱动,间接进行使用的。那么当应用程序使用Open GL去渲染UI的时候,Android应用程序的UI就是通过硬件加速来渲染的,也就是说此处的Open GL线程就是处理硬件加速绘制的线程。


在Android3.0的时候,UI的绘制就支持硬件加速了,不过此时默认是关闭的,应该是发展初期并不稳定。当Android4.0之后,UI的绘制默认开启硬件加速,此时支持应该相对完善了,到了Android5.0出现了Render Thread专门处理硬件加速相关的绘制。


通过上面的论述我们可以简单的得到一个结论,在Android5.0之前无论计算还是绘制都是在主线程上的,而之后,对于启用了硬件加速的UI来说,至少绘制可以在Render Thread上进行。这也就是为什么Android 5.0之后动画更加酷炫了,但UI似乎比之前更加流畅的原因。


至此,我们只是简单的了解了RenderThread是什么,但是对于它如何在Android的绘制中发挥作用,又是如何可以异步渲染动画还为提及。为了解决这些疑问,下面我们来探索Android UI的绘制流程。

  Android UI的绘制流程



对于Android的UI绘制,基本上可以分两步进行。第一步是Android的应用程序进程中进行,第二步在SurfaceFlinger进程中进行。第一步的时候,将UI绘制到图形缓存区域中,然后交由第二步合成以及显示到屏幕中。对于Android的UI绘制,区分基于软件的绘制和硬件加速绘制两种,下面先介绍下软件绘制。

软件绘制

在讲解之前,先看图1,对软件绘制有个整体认识。


图1


在基于软件的绘制模式下,CPU主导绘图,视图依照两个步骤绘制UI:

1)让View层次结构失效;

2)绘制View层次结构。

当应用程序需要更新UI时,都会调用内容发生改变的View的invalidate方法,让其失效的请求会在View对象层次结构中传递,以便计算出需要重绘的屏幕区域(脏区)。然后,Android系统会在View层次结构中绘制所有的跟脏区相交的区域。

在Android应用中每个窗口都关联一个Surface,当需要绘制UI的时候,会调用对应的Surface的lockCanvas方法获取一个Canvas,其本质就是通过SurfaceFlinger 服务Dequeue一个GraphicBuffer。绘制完成后,Android应用调用对应Surface的unlockCanvasAndPost方法请求显示在屏幕中,其本质是向SurfaceFlinger服务Dequeue一个GraphicBuffer,以便SurfaceFlinger服务可以对Graphic Buffer的内容进行合成以及显示到屏幕上去。对于View中onDraw方法中的参数Canvas来说,它便是Surface通过lockCanvas方法获取的Canvas。对于软件绘制来说,它也有不可避免的缺陷:

1)绘制了不需要重绘的视图(与脏区相交的区域);

2)掩盖了一些应用缺陷(因为重绘制了内容)。


至此,我们已经了解了Android中基于软件绘制的流程,正因为软件绘制还有许多不完美,才促使了硬件加速绘制的产生,下面我们讲一下基于硬件加速绘制的流程。

硬件加速绘制

在讲解之前,我们先看图2,对硬件加速绘制有个整体认识。

图2

在基于硬件加速的绘制模式下,GPU主导绘图,绘制按照三个步骤进行:1)让View层次结构失效;2)记录更新显示列表;3)绘制显示列表。这种模式下,Android系统依然会使用invalidate方法和draw方法请求屏幕更新和展示View。但是Android系统并不是立即执行绘制命令,而是首先把这些View的绘制函数作为绘制指令记录在显示列表内,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。且Android系统只需要针对由invalidate方法调用所标记的View对象的脏区进行记录和更新显示列表。没有失效的View对象则能重放先前显示列表记录的绘制指令来进行简单的重绘工作。使用显示列表的目的是,把视图的各种绘制函数翻译成绘制指令保存起来,对于没有发生改变的视图把原先保存的操作指令重新读取出来重放一次就可以了,提高了视图的显示速度。而对于需要重绘的View,则更新显示列表,以便下次重用,然后再调用Open GL完成绘制。硬件加速绘制和软件绘制基本流程一样,在开始绘制之前,都需要向SurfaceFlinger服务Dequeue一个Graphic Buffer。不过对硬件加速绘制来说,这个Graphic Buffer会被封装成一个ANativeWindow,并且传递给Open GL进行硬件加速渲染环境初始化。在Android系统中,ANativeWindow和Surface可以是认为等价的,只不过是ANativeWindow常用于Native层中,而Surface常用于Java层中。另外,我们还可以将ANativeWindow和Surface看作是像Skia和Open GL这样图形渲染库与操作系统底层的图形系统建立连接的一个桥梁。

对于硬件加速来说,Android 5.0之后由于引入了Render Thread,其绘制过程发生了些许的变化,看图3。

图3

在Android 5.0之前的时候,UI发生变化后,主线程内首先要更新对应View的展示列表,当VSYNC信号到来时候,将对应的展示列表转化为对应的OpenGL命令然后调用OpenGL 的函数完成绘制,也就是说主线程既需要完成展示列表的构建工作,又需要通过展示列表转为OpenGL命令完成绘制工作。在Android 5.0 之后,当UI发生改变后,在主线程中更新对应View的展示列表,在VSYNC信号到来时,向Render Thread发出drawFrame的命令,Render Thread内部有一个Task Queue用于接收绘制命令,接收后等待Render Thread的处理。也就是说Android 5.0之后主线程仍旧负责View的展示列表的更新,但绘制交由Render Thread。

至此,我们已经知晓,RenderThread作为Android 5.0的产物,是当UI开启硬件加速后,用于分担主线程绘制任务的渲染线程。这里我们可以得到两个结论:1)Android5.0之后,UI绘制可以异步绘制;2)绘制可以异步,动画可以异步也似乎成为可能,因为阻碍动画异步的主要原因就是UI绘制。但知道了这些,仍不清楚RenderThread对于动画有什么作用,不过,正因为有了这些知识的积累,才能继续后续的探索,好,带着疑问,下面开始探索Android中动画如何使用硬件加速。


 Android中动画使用硬件加速


在讲解之前,我们先看下图4。

图4

动画中使用硬件加速有两种方式,一种是通过View Layer,使用Frame Buffer Object的方式绘制,第二种就是通过将动画注册到RenderThread中进行构建和绘制。下面开始分析第一种。


通过View Layer,使用Frame Buffer Object的方式绘制。View Layer又称为离屏缓冲,它的作用就是将绘制的结果进行缓存,缓存的结果可能是OpenGL的纹理或者是Bitmap,取决于硬件加速是否开启。当UI开启硬件加速的时候,开启动画时设置 setLayerType(LAYER_TYPE_HARDWARE,null),并使用buildLayer方法构建View的Layer,并在动画结束后,使用setLayerType(LAYER_TYPE_NONE, null);进行恢复Layer。此时动画的每一帧都可以通过Open GL的Frame Buffer Object的方式绘制。此处的绘制也和上面提及的一样,也只能是Android 5.0后可以使用Render Thread 绘制,否则仍是主线程中绘制,而且并不能完全异步渲染,还需依赖主线程构建UI的显示列表。但只要使用View Layer,无论是否开启硬件加速,绘制时都会对原有内容重用,提升绘制效率。但是只针对View内容不发生变更的操作,也就是View中无需使用 invalidate方法的操作,比如位移、旋转、透明度等操作。此方案能提高动画的绘制效率但并不是我们想要的,继续看下一种。


通过将动画注册到Render Thread中进行构建和绘制:将待执行的动画注册到 Render Thread 中的AnimatorManager 之后,AnimatorManager开始检测动画是否完成结束,如果还没有结束,那么Render Thread就自动地计算和显示动画的下一帧,直到动画显示结束为止。此时Canvas由DisplayListCanvas实现,方法参数类型使用CanvasProperty对象替换原有的基本类型(例如CanvasProperty<Float>替换float)。此时动画的初始化以及初始的显示列表以及对应的绘制操作在UI线程中创建,之后Render Thread可以通过CanvasProperty异步的进行修改,从而使得后续的显示列表的更新以及绘制都可以由RenderThread进行,从而达到异步渲染动画的目的。很明显,我们终于找到了,这个就是我们要找的。


至此我们终于明白了,在开启硬件加速的时候,Android5.0之前没有RenderThread,主线程需要参与构建和绘制,Android5.0之后出现的RenderThread,可以分担主线程的绘制任务,而动画可以注册到RenderThread中,让RenderThread自己完成构建和绘制,不需依赖主线程。但仅仅如此吗?理论还需实践的支撑,下面我们通过代码的探索来印证理论,然后实现RenderThread渲染动画,来为此次探索,画上完美的句号。


 RenderThread异步渲染动画实践


经过查阅官方文档,得知目前支持RenderThread完全渲染的动画,只有两种:ViewPropertyAnimator和CircularReveal(揭露动画)。对于CircularReveal(揭露动画)使用比较简单且功能较为单一,在此不做过多的探索,着重讲解下ViewPropertyAnimator。

从源码逆推实现

通过刚才的知识,我们了解到如果要实现RenderThread完全渲染动画,只需要将动画注册到RenderThread的AnimatorManager中即可,其余的系统会处理。由于ViewPropertyAnimator并不是任何情况下都是RenderThread完全渲染的动画,我们尝试从源码中探究ViewPropertyAnimator什么情况下,才能实现RenderThread完全渲染的动画,从源码逆推实现。

首先,我们看看如何使用ViewPropertyAnimator呢?

View view = findViewById(R.id.button); ViewPropertyAnimator animator = view.animate().scaleX(1).translationX(1).alpha(1); animator.start();

我们可以看到View调用animate方法就可以创建ViewPropertyAnimator 的动画,它有缩放、位移、透明度相关的方法。但需要注意的是ViewPropertyAnimator 并不是Animator的子类。然后看下ViewPropertyAnimator 的start方法:

public class ViewPropertyAnimator {          ......          public void start() {              ......              startAnimation();          }          ......      }

可以看到,start方法内调用了startAnimation方法:

public class ViewPropertyAnimator {          ......          /**         * A RenderThread-driven backend that may intercept startAnimation         */          private ViewPropertyAnimatorRT mRTBackend;          ......          private void startAnimation() {              if (mRTBackend != null && mRTBackend.startAnimation(this)) {                  return;              }              ......              ValueAnimator animator = ValueAnimator.ofFloat(1.0f);              ......              animator.start();          }          ......      }

通过代码我们可以看到,startAnimation方法首先对mRTBackend 进行了判断,判断不为null的话,则直接由它执行动画。如果判断失败,则继续执行后续的语句,其中mRTBackend是ViewPropertyAnimatorRT 类型的。很明显判断失败后执行的就是属性动画,此时并不支持RenderThread完全渲染,这也就是不满足的情况。再结合mRTBackend 的注释,很明显RenderThread渲染动画应该和ViewPropertyAnimatorRT相关,我们得到首要条件,mRTBackend不为null。

下面开始探索ViewPropertyAnimatorRT#startAnimation方法来进一步确认:

   class ViewPropertyAnimatorRT {          ......          public boolean startAnimation(ViewPropertyAnimator parent) {              ......              if (!canHandleAnimator(parent)) {                  return false;              }              doStartAnimation(parent);              return true;          }          ......      }

startAnimation方法先使用canHandleAnimator方法判断,判断返回false直接退出,返回true后,再执行doStartAnimation方法,我们首先看下canHandleAnimator方法:

   class ViewPropertyAnimatorRT {          ......          private boolean canHandleAnimator(ViewPropertyAnimator parent) {              ......              if (parent.getUpdateListener() != null) {                  return false;              }              if (parent.getListener() != null) {                  // TODO support                  return false;              }              if (!mView.isHardwareAccelerated()) {                  // TODO handle this maybe?                  return false;              }              if (parent.hasActions()) {                  return false;              }              // Here goes nothing...              return true;          }          ......      }

通过此处的代码可以看到,如果执行动画的View不支持硬件加速,或者动画设置了监听Listener或者UpdateListener,或者动画设置了Action(动画开始结束的监听),都会导致返回false,从而导致doStartAnimation就无法执行。此处我们得到进一步的条件,不进行各种设置,确保 canHandleAnimator返回true。

下面继续看一下doStartAnimation方法:

   class ViewPropertyAnimatorRT {          ......          private void doStartAnimation(ViewPropertyAnimator parent) {              int size = parent.mPendingAnimations.size();              ......              for (int i = 0; i < size; i++) {                  NameValuesHolder holder = parent.mPendingAnimations.get(i);                  int property = RenderNodeAnimator.mapViewPropertyToRenderProperty(holder.mNameConstant);                  final float finalValue = holder.mFromValue + holder.mDeltaValue;                  RenderNodeAnimator animator = new RenderNodeAnimator(property, finalValue);                  animator.setStartDelay(startDelay);                  animator.setDuration(duration);                  animator.setInterpolator(interpolator);                  animator.setTarget(mView);                  animator.start();                  mAnimators[property] = animator;              }              parent.mPendingAnimations.clear();          }          ......      }

doStartAnimation方法对于每个动画属性都创建了RenderNodeAnimator,然后将对应的动画参数也设置给了RenderNodeAnimator。此处也就完成了动画和属性的绑定。

这样代码就探索到了RenderNodeAnimator,经过查看发现setTarget方法比较重要,下面看下它的代码:

   public class RenderNodeAnimator extends Animator {          ......          public void setTarget(View view) {              mViewTarget = view;              setTarget(mViewTarget.mRenderNode);          }          private void setTarget(RenderNode node) {              ......              mTarget = node;              mTarget.addAnimator(this);          }          ......      }

我们可以看到setTarget方法将当前View的RenderNode和RenderNodeAnimator通过addAnimator进行绑定。在RenderNode#addAnimator方法中调用了Native方法nAddAnimator,我们看下其Natvie代码的实现:

   static void android_view_RenderNode_addAnimator(JNIEnv* env, jobject clazz,              jlong renderNodePtr, jlong animatorPtr) {          RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);          RenderPropertyAnimator* animator = reinterpret_cast<RenderPropertyAnimator*>(animatorPtr);          renderNode->addAnimator(animator);      }      void RenderNode::addAnimator(const sp<BaseRenderNodeAnimator>& animator) {          mAnimatorManager.addAnimator(animator);      }

至此,我们了解了动画如何被添加到AnimatorManager中。根据刚才的知识,后续AnimatorManager和RenderThread的操作交由系统去处理,进而让RenderThread去完全管理动画,实现RenderThread渲染动画。当然这不是探索的终点,对于RenderThread同AnimatorManager如何交互,又是如何绘制,我们并没有详细的叙述,这不是探索的重点,在此不再细究,如有兴趣可自行继续深入。

从原理到代码实践

我们通过ViewPropertyAnimatorRT#canHandleAnimator方法知道,为了使其方法返回True,一定要满足,不设置各种回调,且View支持硬件加速。

我们可以看到ViewPropertyAnimatorRT 是让动画交由RenderThread处理的关键,但是翻阅源码,并未发现何处创建了这个对象,为了能达到预期的效果,我们自己进行设置。通过查看源码,此类是属于包保护级别的,不是hide的类,在Android P 即使使用反射并不会有影响,下面看下代码实现:

创建对应的view的ViewPropertyAnimatorRT 。

   /**     * 创建对应的View的ViewPropertyAnimatorRT     */    private static Object createViewPropertyAnimatorRT(View view) {        try {            Class<?> animRtClazz = Class.forName("android.view.ViewPropertyAnimatorRT");            Constructor<?> animRtConstructor = animRtClazz.getDeclaredConstructor(View.class);            animRtConstructor.setAccessible(true);            Object animRt = animRtConstructor.newInstance(view);            return animRt;        } catch (Exception e) {            Log.d(TAG, "创建ViewPropertyAnimatorRT出错,错误信息:" + e.toString());            return null;        }    }

然后为viewpropertyAnimator设置对应的mRTBackend的值。

private static void setViewPropertyAnimatorRT(ViewPropertyAnimator animator, Object rt) {        try {            Class<?> animClazz = Class.forName("android.view.ViewPropertyAnimator");            Field animRtField = animClazz.getDeclaredField("mRTBackend");            animRtField.setAccessible(true);            animRtField.set(animator, rt);        } catch (Exception e) {            Log.d(TAG, "设置ViewPropertyAnimatorRT出错,错误信息:" + e.toString());        }    }

在动画执行前需要执行的方法。

/**     * 在执行动画开始前配置     */    public static void onStartBeforeConfig(ViewPropertyAnimator animator, View view) {        Object rt = createViewPropertyAnimatorRT(view);        setViewPropertyAnimatorRT(animator, rt); }

我们看下使用RenderThread渲染动画的完整代码。

private void startAnim2(View v) {        v.setScaleY(1);        ViewPropertyAnimator animator = v.animate().scaleY(2).setDuration(2000);        AnimHelper.onStartBeforeConfig(animator, v);        animator.start();    }

为了达到测试的效果,我们在执行动画期间使用Thread.sleep模拟线程卡顿。完整的RenderThread渲染动画的测试代码如下:

    findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                startAnim2(v);                delaySleep(1000, 3000);            }        });

我们在动画执行期间模拟UI线程卡顿的效果。

private void delaySleep(long delay, final long duration) {        handler.postDelayed(new Runnable() {            @Override            public void run() {                try {                    Thread.sleep(duration);                } catch (InterruptedException e) {                    e.printStackTrace();                }            }        }, delay);    }

为了对比实现效果,另一种实现:

   private void startAnim1(View v) {        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(v, "scaleY", 1, 2);        objectAnimator.setDuration(2000);        objectAnimator.start();    }

另一种实现的完整代码。

    //属性动画        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                startAnim1(v);                delaySleep(1000, 3000);            }        });

此时运行代码,执行不同的动画,就会看到不同的效果,具体效果可参考此GIF效果,如图5。

图5

可以很明显的看到一般的动画,当主线程阻塞的时候,会出现明显的丢帧卡顿,而使用RenderThread渲染的动画即使阻塞了主线程仍不受影响。

至此,已经完成对RenderThread异步渲染动画的探索,如有不同观点,还请留言指正。


长按识别图中二维码立即关注

: . Video Mini Program Like ,轻点两下取消赞 Wow ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存